Chapter 4 — COVID vaccination data (Python supplement)¶

Condensed notebook for the Python / Plotly portion of Chapter 4: COVID Data.

Target graphics:

  • Absolute death risk by age and vaccination status (Figures 4.26–4.27)
  • Relative risk (Figure 4.28)
  • Combined death rate by vaccine status only (Figure 4.29)

Data: Covid.csv in Data For Condensed Notebooks.

Dependencies:

  • pandas
  • plotly

Imports and display options¶

In [1]:
import pandas as pd
import plotly.express as px
In [2]:
# Set output options.
import plotly.io as pio
pio.renderers.default = "pdf+jupyterlab+notebook"
In [3]:
pd.options.mode.copy_on_write = True
/var/folders/_x/nbw2t83x75d0lr3jn4pg_cx00000gn/T/ipykernel_25991/2126689829.py:1: Pandas4Warning: The 'mode.copy_on_write' option is deprecated. Copy-on-Write can no longer be disabled (it is always enabled with pandas >= 3.0), and setting the option has no impact. This option will be removed in pandas 4.0.
  pd.options.mode.copy_on_write = True

Loading the data¶

In [4]:
data_link = '../Data For Condensed Notebooks/Covid.csv'
df = pd.read_csv(data_link)

Grouping and reshaping¶

groupby → value_counts → unstack outcomes; compute rates with crosstab.

In [5]:
grouped = df.groupby(['age_group', 'vaccine_status', 'outcome'])
grouped = pd.DataFrame(df.groupby(['age_group', 'vaccine_status', 'outcome']).value_counts())
grouped = grouped.unstack(level=2)
grouped.columns = grouped.columns.get_level_values(1)
In [6]:
rates = pd.crosstab(index=[df['age_group'], df['vaccine_status']],
                    columns=df['outcome'],
                    normalize='index')
rates.columns = ['death_rate', 'survival_rate']
risk = rates.unstack(level=1)
death_risk = risk['death_rate']
death_risk['relative_risk'] = death_risk['unvaccinated'] / death_risk['vaccinated']
death_risk
Out[6]:
vaccine_status unvaccinated vaccinated relative_risk
age_group
50 + 0.059593 0.016845 3.537623
under 50 0.000325 0.000234 1.390626

Absolute risk bar charts¶

Wide form, then long form after melt. Figure 4.26 (wide); Figure 4.27 (long).

In [7]:
abs_risk = death_risk[['unvaccinated', 'vaccinated']]
fig = px.bar(abs_risk,
             text=['unvaccinated', 'vaccinated'],
             labels={'value': 'Death Rate',
                       'age_group': 'Age Group',
                       'vaccine_status': 'Vaccination Status'},
             barmode='group',
             text_auto='.2%',
             title='Death Rate by Age Group and Vaccination Status')
fig.update_yaxes(tickformat='.2%')
fig
In [8]:
abs_risk_long = pd.DataFrame(abs_risk.stack(level=-1))
abs_risk_long.reset_index(inplace=True)
abs_risk_long.columns = ['age_group', 'vaccination_status', 'percent_dead']
fig = px.bar(abs_risk_long, x='age_group',
             y='percent_dead',
             color='vaccination_status',
             text='percent_dead',
             barmode='group',
             labels={'percent_dead': 'Death Rate',
                       'age_group': 'Age Group',
                       'vaccination_status': 'Vaccination Status'},
             text_auto='.2%')
fig.update_yaxes(tickformat='.2%')
fig

Relative risk bar chart¶

Reference line at 1 with annotation boxes. Figure 4.28.

In [9]:
relative_risk = death_risk[['relative_risk']]
In [10]:
fig = px.bar(relative_risk,
             x=relative_risk.index,
             y='relative_risk',
             text='relative_risk',
             text_auto='.3',
             labels={'age_group': 'Age Group',
                       'relative_risk': 'Relative Risk'},
             title='Relative Risk for Unvaccinated People',
             height=600,
             width=600)
fig.update_traces(marker_color='lightskyblue')
fig.add_hline(y=1, line_dash='dot',
              annotation_text='Vaccine has no effect',
              annotation_position='bottom left')
fig.add_annotation(x=0.5, y=2,
            text='Unvaccinated more likely to die',
            showarrow=True,
            arrowhead=2,
            arrowsize=1,
            arrowwidth=2,
            arrowcolor='#636363',
            ax=0,
            ay=50,
            font=dict(family='Courier New, monospace', size=16, color='#ffffff'),
            bordercolor='#c7c7c7',
            borderwidth=2,
            borderpad=4,
            bgcolor='lightslategray',
            opacity=0.8)
fig.add_annotation(x=0.5, y=0.2,
           text='Unvaccinated less likely to die',
            showarrow=True,
            arrowhead=2,
            arrowsize=1,
            arrowwidth=2,
            arrowcolor='#636363',
            ax=0,
            ay=-50,
            font=dict(family='Courier New, monospace', size=16, color='#ffffff'),
            bordercolor='#c7c7c7',
            borderwidth=2,
            borderpad=4,
            bgcolor='lightslategray',
            opacity=0.8)
fig.update_xaxes(categoryorder='array', categoryarray=['under 50', '50 +'])
fig

Death rate by vaccine status only¶

Aggregate across age groups. Contrasts with the stratified charts above. Figure 4.29.

In [11]:
combined_rates = pd.crosstab(index=df['vaccine_status'], columns=df['outcome'], normalize='index')
combined_rates.columns = ['death_rate', 'survival_rate']
fig = px.bar(combined_rates[['death_rate']],
             labels={'value': 'Death Rate',
                       'vaccine_status': 'Vaccine Status'},
             text_auto='.2%',
             title='Death Rate by Vaccine Status',
             height=600,
             width=400)
fig.update_layout(showlegend=False)
fig.update_yaxes(tickformat='.2%')
fig